package risoe.syslab.fhnew.hw;

/*
 * Copyright (c) 2012-2014, Technical University of Denmark (DTU)
 * All rights reserved.
 * 
 * The Flexhouse 2.0 platform ("this software") is licensed under the
 * BSD 3-clause license.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *   - Neither the name of DTU nor the names of its contributors may be used to
 *     endorse or promote products derived from this software without specific
 *     prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE TECHNICAL UNIVERSITY OF DENMARK BE LIABLE FOR ANY
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import java.lang.reflect.Constructor;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.concurrent.ConcurrentLinkedQueue;

import risoe.syslab.fhnew.common.SystemConstants.DeviceType;
import risoe.syslab.fhnew.config.AbstractIOConfig;
import risoe.syslab.fhnew.config.ActuatorConfig;
import risoe.syslab.fhnew.config.HWInterfaceConfig;
import risoe.syslab.fhnew.config.KNXInterfaceConfig;
import risoe.syslab.fhnew.config.SensorConfig;
import risoe.syslab.fhnew.hw.knx.KNXActuator;
import risoe.syslab.fhnew.hw.knx.KNXConstants;
import risoe.syslab.fhnew.hw.knx.KNXDevice;
import risoe.syslab.fhnew.hw.knx.KNXHandler;
import risoe.syslab.fhnew.hw.knx.KNXHandler.KNX_Task;
import risoe.syslab.fhnew.hw.knx.KNXSensor;
import risoe.syslab.model.CompositeMeasurement;
import risoe.syslab.model.Quality;
import risoe.syslab.model.Source;
import risoe.syslab.model.Validity;
import tuwien.auto.calimero.CloseEvent;
import tuwien.auto.calimero.DetachEvent;
import tuwien.auto.calimero.FrameEvent;
import tuwien.auto.calimero.GroupAddress;
import tuwien.auto.calimero.exception.KNXException;
import tuwien.auto.calimero.exception.KNXFormatException;
import tuwien.auto.calimero.link.KNXNetworkLinkIP;
import tuwien.auto.calimero.link.event.NetworkLinkListener;
import tuwien.auto.calimero.link.medium.TPSettings;
import tuwien.auto.calimero.process.ProcessCommunicator;
import tuwien.auto.calimero.process.ProcessCommunicatorImpl;
import tuwien.auto.calimero.process.ProcessEvent;
import tuwien.auto.calimero.process.ProcessListener;

/**
 * KNX Interface. Spawns handler threads that take care of incoming and outgoing KNX IP packets.
 * @author M. Svendsen
 */
public class KNXInterface extends HardwareInterface implements ConnectionManagerInterface, Runnable{
	private static final int RESPONSE_TIMEOUT = 1;
	private static final int PORT_LOCAL = 3671;
	private InetAddress mIp;
	private InetAddress mHostAddress;
	private int mPort;
	private ProcessCommunicator mProcessCommunicator;
	private KNXNetworkLinkIP mKNXNetworkLinkIP;
	private HashMap<String, KNXDevice> mDeviceNameMap = new HashMap<String, KNXDevice>();
	private HashMap<GroupAddress, KNXDevice> mDeviceAddressMap = new HashMap<GroupAddress, KNXDevice>();
	private boolean mRunning = false;

	// Concurrent Operations
	private final Object mLock = new Object();
	private final ConcurrentLinkedQueue<EventContainer> mEvents = new ConcurrentLinkedQueue<EventContainer>();
	private KNXHandler mKNX_Handler;
	private ConnectionManager mConnectionManager;
	private Thread mThread;

	// Container class used for passing around timestamped KNX events
	private static class EventContainer {
		ProcessEvent mEvent;
		long mTime;
		public EventContainer(ProcessEvent event, long time) {
			mEvent = event;
			mTime = time;
		}
	}

	/**
	 * Creates a new KNX Interface using the IP address stored in the HWInterfaceConfig object
	 * @param the HWInterfaceConfig
	 */
	public KNXInterface(HWInterfaceConfig hic) {
		super(hic);
		mIp = ((KNXInterfaceConfig)hic).getIPAddress();
		mPort = ((KNXInterfaceConfig)hic).getPort();
		mHostAddress = ((KNXInterfaceConfig)hic).getHostAddress();
	}

	@Override
	protected void startInterfaceImpl(ArrayList<SensorConfig> sensors, ArrayList<ActuatorConfig> actuators) {
		mRunning = true;

		// Map all flexhouse sensors and actuators to KNX_Device types
		mapDevices(sensors, actuators);

		mThread = new Thread(this);
		mThread.start();

		// Use a ConnectionManager to handle the connection to the KNX hardware.
		mConnectionManager = new ConnectionManager(this);
		mConnectionManager.startConnection();
	}

	@Override
	public boolean connect(){
		// Init the IP-connection
		try {
			mKNXNetworkLinkIP = new KNXNetworkLinkIP(
					KNXNetworkLinkIP.TUNNEL, 
					new InetSocketAddress(mHostAddress, PORT_LOCAL),
					new InetSocketAddress(mIp, mPort), false, TPSettings.TP1);
			mKNXNetworkLinkIP.addLinkListener(new NetworkLinkListener() {

				@Override
				public void linkClosed(CloseEvent arg0) {
					// Connection was closed - notify ConnectionManager
					mConnectionManager.onDisconnected();
				}

				@Override
				public void indication(FrameEvent arg0) {
					// Ignore
				}

				@Override
				public void confirmation(FrameEvent arg0) {
					// Ignore
				}
			});

			mProcessCommunicator = new ProcessCommunicatorImpl(mKNXNetworkLinkIP);
			mProcessCommunicator.setResponseTimeout(RESPONSE_TIMEOUT);

			// Add a processListener to the newly created ProcessCommunicator
			mProcessCommunicator.addProcessListener(new ProcessListener(){
				@Override
				public void groupWrite(ProcessEvent e) {
					// New events must be processed on the main thread to allow for additional device IO
					// mEvents contains the events that need to be processed
					mEvents.add(new EventContainer(e, System.currentTimeMillis()*1000));

					synchronized (mLock) {
						// Unblock main thread to let it process the new event
						mLock.notify();
					}

				}

				@Override
				public void detached(DetachEvent e) {
					mProcessCommunicator.removeProcessListener(this);
				}
			});

			if (mKNX_Handler == null){
				mKNX_Handler = new KNXHandler();

				// Start handling of async writes
				mKNX_Handler.start();
			}

			return true;
		} catch (KNXException e) {
			return false;
		}
	}

	@Override
	public void disconnect() {
		if (mKNXNetworkLinkIP != null){
			mKNXNetworkLinkIP.close();
			mProcessCommunicator.detach();
			mProcessCommunicator = null;
		}
	}

	@Override
	public boolean probeConnection() {
		// Check connection status
		return mKNXNetworkLinkIP.isOpen();
	}

	@Override
	public boolean isConnected() {
		return mConnectionManager.isConnected();
	}

	/**
	 * Ends the KNX thread/connection
	 */
	@Override
	protected void stopInterfaceImpl() {
		mRunning = false;
		synchronized (mLock) {
			mLock.notifyAll();
		}

		if (mKNX_Handler != null){
			mKNX_Handler.interrupt();
		}
		
		// Stop connection
		mConnectionManager.stopConnection();
	}

	/**
	 * Thread will process any events in the queue and wait if no events are present
	 */
	@Override
	public void run() {
		Thread.currentThread().setName(getName()+"hardware interface thread");
		while (!Thread.interrupted()) {
			// Run until user exits
			if (mRunning){
				synchronized (mLock) {
					// Wait for a new event to be posted
					try {
						mLock.wait();
					} catch (InterruptedException e) {/* Unused */}
				}

				if (mConnectionManager.isConnected()){
					// Check if any events have been posted
					while (!mEvents.isEmpty()){
						synchronized (mEvents) {
							onMessaged(mEvents.poll());
						}
					}
				}
			}
			else{
				Thread.currentThread().interrupt();
			}
		}
	}

	/**
	 * Posts a new task on the associated KNX_Handler, returning a handle to the task. This handle can be used to check completion and so forth.
	 * This function is called internally when a call to an actuators 'write' function is executed.
	 * @param device the device that ordered this write
	 * @param value the value to write
	 * @return a reference to the newly created asynchronous task
	 */
	public KNX_Task postTask(KNXDevice device, String value){
		return mKNX_Handler.postTask(device, value);
	}

	/**
	 * Returns a reference to a KNX_Device subtype. This reference can be used for translation between events and values.
	 * @param a string representing the name of the device.
	 * @return a reference to the KNX_Device instance.
	 */
	private KNXDevice getDevice(String name){
		return mDeviceNameMap.get(name);
	}

	/**
	 * @return a reference to the ProcessCommunicator attached to this KNXInterface
	 */
	public ProcessCommunicator getProcessCommunicator(){
		return mProcessCommunicator;
	}

	/**
	 * Called by the KNX Link Listener registered to this interface. A translated version of the event
	 * will get passed on to the process image, which in turn calls the controllermanager
	 */
	public void onMessaged(EventContainer container) {
		// Call the onMessaged function of the appropriate device
		if (mDeviceAddressMap != null){
			KNXDevice dev = mDeviceAddressMap.get(container.mEvent.getDestination());
			if (dev != null){
				// Update processimage with this new event
				synchronized (mData) {
					mData.clear();
					CompositeMeasurement measurement = new CompositeMeasurement(dev.getDoubleValue(container.mEvent),
							container.mTime, Quality.OK, Validity.GOOD, Source.PROCESS);
					mData.put(dev.getName(), measurement);
					notifyProcessImage();
				}
			}
			else{
				getLogger().logEvent(getName(), "Error", "Unknown device messaged: "+container.mEvent.getDestination());
			}
		}
		else{
			getLogger().logEvent(getName(), "Error", "Device Address Map is null");
		}
	}
	
	@Override
	public CompositeMeasurement read(Sensor sensor) {
		KNXDevice dev = getDevice(sensor.getName());
		if (dev instanceof KNXSensor){
			KNXSensor s = ((KNXSensor) dev);
			String reading = s.read();
			double val = s.getDoubleValue(reading);
			return new CompositeMeasurement(val);
		}
		
		return CompositeMeasurement.INVALID;
	}

	@Override
	public void applyActuation(Actuator actuator) {
		// Get KNX type representation of this actuator
		KNXDevice dev = getDevice(actuator.getName());
		if (dev instanceof KNXActuator){
			if (mConnectionManager.isConnected()){
				((KNXActuator) dev).write(actuator.getSetpoint().value);

				// Notify ProcessImage of successful updating of setpoint
				synchronized (mData) {
					mData.clear();
					CompositeMeasurement measurement = new CompositeMeasurement(actuator.getSetpoint().value, Quality.OK, Validity.GOOD, Source.PROCESS);
					mData.put(actuator.getName(), measurement);
					notifyProcessImage();
				}
			}
			else{
				getLogger().logEvent(getName(), "Error", "Interface not connected - Error applying actuation");
			}
		}
		else{
			getLogger().logEvent(getName(), "Error", "Missing KNX actuator "+actuator.getName());
		}
	}

	/**
	 * Maps all sensors and actuators to the appropriate KNX specific classes. This allows for easy translation
	 * between events and values and a higher level of abstraction.
	 * Should only be called from startInterfaceImpl()
	 */
	public void mapDevices(ArrayList<SensorConfig> sensors,
			ArrayList<ActuatorConfig> actuators){

		// Iterate over sensors and actuators and instantiate objects appropriately
		int size = sensors.size() + actuators.size();
		int offset = sensors.size();
		for (int i=0; i<size; i++){
			AbstractIOConfig device = i<sensors.size()? sensors.get(i): actuators.get(i-offset);
			DeviceType type = device.getType();
			
			// Instatiate a subclass of KNX_Sensor / KNX_Actuator. This class will handle translation to/from KNX IP events
			try {
				Constructor<?> con = KNXConstants.deviceConstructorMap.get(type);
				KNXDevice dev = (KNXDevice) con.newInstance(device.getInterfaceAttributes().get("gaddr"), device, KNXInterface.this);
				
				mDeviceAddressMap.put(dev.getGroupAddress(), dev);
				mDeviceNameMap.put(dev.getName(), dev);

				// Check for aliases and bind them to the same device.
				String aliases[] = device.getAliases("gaddr");
				if (aliases != null){
					for (String alias : aliases){
						String aliasedAddress = device.getInterfaceAttributes().get(alias);
						try {
							mDeviceAddressMap.put(new GroupAddress(aliasedAddress), dev);
						} catch (KNXFormatException e) {
							getLogger().logEvent(getName(), "Formatting Error", "Error creating KNX GroupAddress for device: "+
									dev.getName()+" with address: "+aliasedAddress);
						}
					}
				}
			} catch (Exception e1) {
				System.out.println("Error instantiating KNX device: "+device+" "+e1.getMessage());
				getLogger().logEvent(getName(), "Error", "Problem constructing KNX device: "+e1.getMessage());
			}
		}
	}
}
